/*
 * Copyright 2024 The CHANGLEI Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/// Created by changlei on 2024/4/26.
///
/// 生成代码
library;

import 'package:code_builder/code_builder.dart' as code;
import 'package:dart_style/dart_style.dart';
import 'package:fibber/src/name.dart';

final dartfmt = DartFormatter(
  pageWidth: 1 << 54,
);
final emitter = code.DartEmitter.scoped(
  orderDirectives: true,
  useNullSafetySyntax: true,
);

const indent = r'  ';

sealed class Spec<T extends code.Spec> {
  const Spec();

  T get spec;

  @override
  String toString() => dartfmt.format('${spec.accept(emitter)}');
}

sealed class NameSpec<T extends code.Spec> implements Spec<T>, Comparable<NameSpec<T>> {
  const NameSpec();

  Name get name;

  @override
  int compareTo(covariant NameSpec<T> other) => name.compareTo(other.name);
}

final class Class extends NameSpec<code.Class> {
  const Class({
    required this.name,
    required this.path,
    this.fields = const [],
    this.methods = const [],
    this.constructor = Name.underline,
  });

  @override
  final Name name;
  final String path;
  final List<Field> fields;
  final List<Method> methods;
  final Name? constructor;

  @override
  code.Class get spec {
    return code.Class((builder) {
      builder
        ..name = name
        ..docs.add('/// $path')
        ..fields.addAll(fields.map((e) => e.spec))
        ..methods.addAll(methods.map((e) => e.spec))
        ..constructors.add(
          code.Constructor((builder) {
            builder
              ..name = constructor
              ..constant = true;
          }),
        );
    });
  }
}

final class ExtensionType extends NameSpec<code.ExtensionType> {
  const ExtensionType({
    required this.name,
    required this.path,
    this.fields = const [],
    this.methods = const [],
    this.superClass = Name.string,
    this.constructor = Name.underline,
  });

  @override
  final Name name;
  final String path;
  final Name superClass;
  final List<Field> fields;
  final List<Method> methods;
  final Name? constructor;

  @override
  code.ExtensionType get spec {
    final constructor = this.constructor;
    return code.ExtensionType((builder) {
      builder
        ..name = '$name${constructor == null || constructor.isEmpty ? r'' : '.$constructor'}'
        ..implements.add(code.refer(superClass))
        ..constant = true
        ..docs.add('/// $path')
        ..representationDeclaration = code.RepresentationDeclaration((builder) {
          builder
            ..name = Name.underline * 2
            ..declaredRepresentationType = code.refer(Name.string);
        })
        ..fields.addAll(fields.map((e) => e.spec))
        ..methods.addAll(methods.map((e) => e.spec));
    });
  }
}

sealed class Field extends NameSpec<code.Field> {
  const Field();
}

final class StaticField extends Field {
  const StaticField({
    required this.name,
    required this.type,
    required this.doc,
    required this.constructor,
  });

  @override
  final Name name;
  final Name type;
  final String doc;
  final String constructor;

  @override
  code.Field get spec {
    return code.Field((builder) {
      builder
        ..name = name
        ..static = true
        ..modifier = code.FieldModifier.constant
        ..docs.add('/// $doc')
        ..assignment = code.Code(constructor);
    });
  }
}

final class IconDataField extends Field {
  const IconDataField({
    required this.name,
    required this.path,
    required this.codePoint,
    required this.fontFamily,
    required this.fontPackage,
  });

  @override
  final Name name;
  final String path;
  final String codePoint;
  final Name fontFamily;
  final Name? fontPackage;

  @override
  code.Field get spec {
    return code.Field((builder) {
      final String iconCode;
      if (fontPackage == null) {
        iconCode = 'IconData($codePoint, fontFamily: $fontFamily)';
      } else {
        iconCode = 'IconData($codePoint, fontFamily: $fontFamily, fontPackage: $fontPackage)';
      }
      builder
        ..name = name
        ..static = true
        ..modifier = code.FieldModifier.constant
        ..type = code.refer('IconData')
        ..docs.add('/// $path')
        ..assignment = code.Code(iconCode);
    });
  }
}

final class AssetsField extends Field {
  const AssetsField({
    required this.name,
    required this.path,
    required this.classname,
    required this.package,
  });

  @override
  final Name name;
  final String path;
  final Name classname;
  final String? package;

  @override
  code.Field get spec {
    return code.Field((builder) {
      final String assetCode;
      if (package == null) {
        assetCode = "r'$path'";
      } else {
        assetCode = "r'packages/$package/$path'";
      }
      builder
        ..name = name
        ..static = true
        ..modifier = code.FieldModifier.constant
        ..docs.add('/// $path')
        ..assignment = code.Code('$classname._($assetCode)');
    });
  }
}

sealed class Directive implements Spec<code.Directive> {
  const Directive();

  String get url;
}

final class Import extends Directive {
  const Import(
    this.url, {
    this.as,
    this.show = const [],
    this.hide = const [],
  });

  @override
  final String url;
  final String? as;
  final List<String> show;
  final List<String> hide;

  @override
  code.Directive get spec {
    return code.Directive.import(
      url,
      as: as,
      show: show,
      hide: hide,
    );
  }
}

sealed class Method extends NameSpec<code.Method> {
  const Method();
}

final class Getter extends Method {
  const Getter({
    required this.name,
    required this.returns,
    required this.doc,
    required this.constructor,
    this.static = false,
  });

  @override
  final Name name;
  final Name returns;
  final String doc;
  final String constructor;
  final bool static;

  @override
  code.Method get spec {
    return code.Method((builder) {
      builder
        ..name = name
        ..lambda = true
        ..static = static
        ..docs.add('/// $doc')
        ..type = code.MethodType.getter
        ..returns = code.refer(returns)
        ..body = code.Code(constructor);
    });
  }
}
